/*
 * Copyright © 2017 eqxiu.com 北京中网易企秀科技有限公司  All rights reserved.
 */
package cn.hermit.data.xml.model;

import java.util.Collection;
import java.util.Iterator;

import javax.xml.XMLConstants;
import javax.xml.namespace.QName;

import cn.hermit.data.exception.FactoryException;
import cn.hermit.data.exception.ValidationException;
import cn.hermit.util.StringUtils;
import cn.hermit.data.xml.validator.ValidatorChain;

/**
 * XML Factory to create XML objects
 * 
 * <p>
 * Some cases requires to keep order of XML nodes or XML attributes.
 * The framework provides many factories to create various kinds of XML element, which keeps nodes or attributes order.
 * Please see the package of {@link com.eqxiu.tiny.data.xml.factory various factories }
 * </p>
 * 
 * @author Jack Gao (Chinese name : GAO JIANGUO, Email : linux.gjg@gmail.com)
 * @date 19 Jun, 2014
 */
public abstract class XmlFactory {

	private ValidatorChain validator;

	/**
	 * Constructor of XmlFactory
	 * 
	 * <p>
	 * Initialize the validator chain inside the constructor
	 * </p>
	 */
	public XmlFactory() {
		validator = new ValidatorChain();
	}

	/**
	 * Create a container of sub XML Nodes
	 * 
	 * @return
	 * 		a container of sub XML Nodes
	 */
	public abstract Collection<XmlNode> createChildrenContainer();

	/**
	 * Create a container of XML Attributes
	 * 
	 * @return
	 * 		a container of XML Attributes
	 */
	public abstract Collection<XmlAttribute> createAttributeContainer();

	/**
	 * Create XML Document.
	 * 
	 * @return
	 * 		XML Document
	 */
	public XmlDocument createDocument() {
		return createDocument(null, null);
	}

	/**
	 * Create XML Document with version and encoding.
	 * 
	 * @param version
	 * 		The version
	 * @param encoding
	 * 		The encoding
	 * @return
	 * 		XML Document
	 */
	public XmlDocument createDocument(String version, String encoding) {
		XmlDocument document = new XmlDocument();
		document.children = createChildrenContainer();
		if (version == null && encoding == null)
			return document;
		XmlHeader header = new XmlHeader();
		header.version = version;
		header.encoding = encoding;
		document.header = header;
		return document;
	}

	/**
	 * Assign a version for XML Document.
	 * 
	 * @param document
	 * 		The XML Document
	 * @param version
	 * 		The version
	 */
	public void version(XmlDocument document, String version) {
		if (document == null || StringUtils.isEmpty(version)) {
			return;
		}
		if (document.header == null)
			document.header = new XmlHeader();
		document.header.version = version;
	}

	/**
	 * Assign a encoding for XML Document.
	 *  
	 * @param document
	 * 		The XML Document
	 * @param encoding
	 * 		The encoding
	 */
	public void encoding(XmlDocument document, String encoding) {
		if (document == null || StringUtils.isEmpty(encoding)) {
			return;
		}
		if (document.header == null)
			document.header = new XmlHeader();
		document.header.encoding = encoding;
	}

	/**
	 * Assign an attribute standalone to XML Document.
	 * 
	 * @param document
	 * 		The XML Document
	 * @param standalone
	 * 		The standalone attribute
	 */
	public void standalone(XmlDocument document, String standalone) {
		if (document == null || StringUtils.isEmpty(standalone)) {
			return;
		}
		if (document.header == null)
			document.header = new XmlHeader();
		document.header.standalone = standalone;
	}

	/**
	 * Add a XML Header for XML Document.
	 * 
	 * @param document
	 * 		The XML Document
	 * @param header
	 * 		The XML Header
	 */
	public void header(XmlDocument document, XmlHeader header) {
		if (document == null)
			return;
		document.header = header;
	}

	/**
	 * Add a XML Header for XML Document with version, encoding and standalone attribute.
	 * 
	 * @param document
	 * 		The XML Document
	 * @param version
	 * 		The version in XML Header
	 * @param encoding
	 * 	 	The encoding in XML Header
	 * @param standalone
	 * 		The standalone attribute in XML Header
	 */
	public void header(XmlDocument document, String version, String encoding,
			String standalone) {
		header(document, createHeader(version, encoding, standalone));
	}

	/**
	 * Create XML Header with version, encoding and standalone attribute.
	 * 
	 * @param version
	 * 		The version
	 * @param encoding
	 * 		The encoding
	 * @param standalone
	 * 		The standalone attribute
	 * @return
	 * 		A XML Header
	 */
	public XmlHeader createHeader(String version, String encoding,
			String standalone) {
		XmlHeader header = new XmlHeader();
		if (!StringUtils.isEmpty(version)) {
			header.version = version;
		}
		if (!StringUtils.isEmpty(encoding)) {
			header.encoding = encoding;
		}
		if (!StringUtils.isEmpty(standalone)) {
			header.standalone = standalone;
		}
		return header;
	}

	/**
	 * Add root XML Element for XML Document.
	 * 
	 * @param document
	 * 		The XML Document
	 * @param root
	 * 		The root XML Element
	 */
	public void root(XmlDocument document, XmlElement root) {
		if (root == null)
			return;
		document.root = root;
		if (!document.children.isEmpty()) {
			Iterator<XmlNode> iter = document.children.iterator();
			while (iter.hasNext()) {
				if (iter.next().isElement()) {
					iter.remove();
				}
			}
		}
		document.children.add(root);
		root.parent = document;
	}

	/**
	 * Create XML Element with QName
	 * @param name
	 * 		The QName
	 * @return
	 * 		A XML Element
	 */
	public XmlElement createElement(QName name) {
		return createElement(name, false);
	}

	/**
	 * Create XML Element with QName
	 * 
	 * @param name
	 * 		The QName
	 * @param closedTag
	 * 		If true, the new Element will be tag-closed element
	 * @return
	 * 		A XML Element
	 */
	public XmlElement createElement(QName name, boolean closedTag) {
		return createElement(name, null, closedTag);
	}

	/**
	 * Create XML Element with QName and Text node
	 * @param name
	 * 		The QName
	 * @param content
	 * 		Content of The Text node
	 * @return
	 * 		A XML Element
	 */
	public XmlElement createElement(QName name, Object content) {
		return createElement(name, content, false);
	}

	/**
	 * Create XML Element with QName and Text node
	 * @param name
	 * 		The QName
	 * @param content
	 * 		Content of The Text node
	 * @param closedTag
	 * 		If true, the new Element will be tag-closed element
	 * @return
	 * 		A XML Element
	 */
	private XmlElement createElement(QName name, Object content,
			boolean closedTag) {
		try {
			name = validator.validateQName(name);
		} catch (ValidationException e) {
			throw new FactoryException(e);
		}
		if (name == null)
			return null;
		XmlElement elem = new XmlElement();
		elem.closedTag = closedTag;
		elem.name = name;
		elem.attributes = this.createAttributeContainer();
		elem.children = this.createChildrenContainer();
		text(elem, content);
		return elem;
	}

	/**
	 * Append a sub element with QName to XML Element.
	 * 
	 * @param elem
	 * 	 	The XML Element
	 * @param name
	 * 		QName of a sub element
	 */
	public void element(XmlElement elem, QName name) {
		element(elem, name, true);
	}

	/**
	 * Append a sub element with QName to XML Element.
	 * 
	 * @param elem
	 * 		The XML Element
	 * @param name
	 * 		QName of a sub element
	 * @param needRefreshQName
	 * 		If true, will refresh QNames of the sub element
	 */
	public void element(XmlElement elem, QName name, boolean needRefreshQName) {
		element(elem, name, null, needRefreshQName);
	}
	
	/**
	 * Append a sub element with QName and Text node to XML Element.
	 * 
	 * @param elem
	 * 		The XML Element
	 * @param name
	 * 		QName of a sub element
	 * @param content
	 * 		Content of the Text node
	 */
	public void element(XmlElement elem, QName name, Object content) {
		element(elem, name, content, true);
	}

	/**
	 * Append a sub element with QName and Text node to XML Element.
	 * 
	 * @param elem
	 * 		The XML Element
	 * @param name
	 * 		QName of a sub element
	 * @param content
	 * 		Content of the Text node
	 * @param needRefreshQName
	 * 		If true, will refresh QNames of the sub element
	 */
	public void element(XmlElement elem, QName name, Object content,
			boolean needRefreshQName) {
		XmlElement child = createElement(name, content);
		child(elem, child, needRefreshQName);
	}

	/**
	 * Set the XML Element as tag-closed.
	 * 
	 * @param elem
	 * 		The XML Element
	 */
	public void closeElement(XmlElement elem) {
		elem.closedTag = true;
	}

	/**
	 * Append a attribute with QName and value to XML Element.
	 * 
	 * @param elem
	 * 		The XML Element
	 * @param name
	 * 		QName of the attribute
	 * @param value
	 * 		Value of the attribute
	 */
	public void attribute(XmlElement elem, QName name, Object value) {
		attribute(elem, name, value, true);
	}

	/**
	 * Append a attribute with QName and value to XML Element.
	 * 
	 * @param elem
	 * 		The XML Element
	 * @param name
	 * 		QName of the attribute
	 * @param value
	 * 		Value of the attribute
	 * @param needRefreshQName
	 * 		If true, will refresh QNames of the XML Element
	 */
	public void attribute(XmlElement elem, QName name, Object value,
			boolean needRefreshQName) {
		if (elem == null || name == null || value == null)
			return;
		XmlAttribute attribute = createAttribute(name, value);
		attribute(elem, attribute, needRefreshQName);
	}

	/**
	 * Append an attribute to XML Element.
	 * 
	 * @param elem
	 * 		The XML Element
	 * @param attribute
	 * 		An attribute
	 */
	public void attribute(XmlElement elem, XmlAttribute attribute) {
		attribute(elem, attribute, true);
	}
	
	/**
	 * Append an attribute to XML Element.
	 * 
	 * @param elem
	 * 		The XML Element
	 * @param attribute
	 * 		An attribute
	 * @param needRefreshQName
	 * 		If true, will refresh QNames of the XML Element
	 */
	public void attribute(XmlElement elem, XmlAttribute attribute,
			boolean needRefreshQName) {
		if (elem == null || attribute == null)
			return;
		if (!elem.isElement()) {
			throw new UnsupportedOperationException(
					"Xml document should has no attribute");
		}
		attribute.parent = elem;
		elem.attributes.add(attribute);
		if (needRefreshQName)
			refreshQNames(elem, attribute);
	}

	/**
	 * Refresh QName of XML Element by XML Attribute
	 * @param elem
	 * 		The XML Element
	 * @param attribute
	 * 		The XML Attribute which may contain NameSpace definition
	 */
	private void refreshQNames(XmlElement elem, XmlAttribute attribute) {
		if (XMLConstants.XMLNS_ATTRIBUTE.equals(attribute.name.getLocalPart())) {
			if (!StringUtils.isEmpty(elem.name.getNamespaceURI())
					&& !elem.name.getNamespaceURI().equals(
							attribute.name.getNamespaceURI())) {
				if (StringUtils.isAllEmpty(attribute.name.getPrefix(),
						elem.name.getPrefix())) {
					elem.name = new QName(attribute.name.getNamespaceURI(),
							elem.name.getLocalPart(), elem.name.getPrefix());
				} else if (!StringUtils.isEmpty(attribute.name.getPrefix())
						&& attribute.name.getPrefix().equals(
								elem.name.getPrefix())) {
					elem.name = new QName(attribute.name.getNamespaceURI(),
							elem.name.getLocalPart(), elem.name.getPrefix());
				}
			}
		} else {
			// TODO Auto-generated method stub
		}
	}

	/**
	 * Create XML Attribute with QName and value.
	 * 
	 * @param name
	 * 		The QName
	 * @param value
	 * 		The value
	 * @return
	 * 		A XML Attribute
	 */
	public XmlAttribute createAttribute(QName name, Object value) {
		try {
			name = validator.validateQName(name);
			value = validator.validateValue(value);
		} catch (ValidationException e) {
			throw new FactoryException(e);
		}
		XmlAttribute attribute = new XmlAttribute();
		attribute.name = name;
		attribute.value = value;
		return attribute;
	}

	/**
	 * Append a sub node to XML Element.
	 * 
	 * @param elem
	 * 		The XML Element
	 * @param child
	 * 		A sub node
	 */
	public void child(XmlElement elem, XmlNode child) {
		child(elem, child, true);
	}

	/**
	 * Append a sub node to XML Element.
	 * 
	 * @param elem
	 * 		The XML Element
	 * @param child
	 * 		A sub node
	 * @param needRefreshQName
	 * 		If true, will refresh QNames of the XML Element
	 */
	public void child(XmlElement elem, XmlNode child, boolean needRefreshQName) {
		if (elem == null || child == null)
			return;
		if (XmlDocument.class.isInstance(elem)
				&& !XmlComment.class.isInstance(child)) {
			throw new FactoryException("XML Document can only append comments");
		}
		elem.closedTag = false;
		child.parent = elem;
		elem.children.add(child);
		if (needRefreshQName && XmlElement.class.isInstance(child)) {
			refreshQNames(elem, child);
		}
	}

	/**
	 * Update QName of XML NamedNode.
	 * 
	 * @param node
	 * 		The XML NamedNode
	 * @param newName
	 * 		A new QName
	 */
	public void updateName(XmlNameNode node, QName newName) {
		node.name = newName;
	}

	private void refreshQNames(XmlElement elem, XmlNode child) {
		// TODO Auto-generated method stub

	}

	/**
	 * Append a text node to XML Element.
	 * 
	 * @param elem
	 * 		The XML Element
	 * @param content
	 * 		Content of the text node
	 */
	public void text(XmlElement elem, Object content) {
		if (elem == null)
			return;
		if (content == null)
			return;
		XmlText text = createText(content);
		child(elem, text, false);
	}

	/**
	 * Create a XML Text node.
	 * 
	 * @param content
	 * 		Content of the XML Text node
	 * @return
	 * 		A XML Text node
	 */
	public XmlText createText(Object content) {
		try {
			content = validator.validateText(content);
		} catch (ValidationException e) {
			throw new FactoryException(e);
		}
		XmlText text = new XmlText();
		text.content = content;
		return text;
	}

	/**
	 * Append a CDATA node to XML Element.
	 * 
	 * @param elem
	 * 		The XML Element
	 * @param content
	 * 		Content of the CDATA node
	 */
	public void cdata(XmlElement elem, Object content) {
		if (elem == null)
			return;
		if (content == null)
			return;
		XmlCData cdata = createCdata(content);
		child(elem, cdata, false);
	}

	/**
	 * Create a XML CDATA node.
	 * 
	 * @param content
	 * 		Content of the XML CDATA node
	 * @return
	 * 		A XML CDATA node
	 */
	public XmlCData createCdata(Object content) {
		try {
			content = validator.validateCData(content);
		} catch (ValidationException e) {
			throw new FactoryException(e);
		}
		XmlCData cdata = new XmlCData();
		cdata.content = content;
		return cdata;
	}

	/**
	 * Append a comment node to XML Element.
	 * 
	 * @param elem
	 * 		The XML Element
	 * @param content
	 * 		Content of the comment node
	 */
	public void comment(XmlElement elem, Object content) {
		if (elem == null)
			return;
		if (content == null)
			return;
		XmlComment comment = createComment(content);
		child(elem, comment, false);
	}

	/**
	 * Create a XML comment node.
	 * 
	 * @param content
	 * 		Content of the XML comment node
	 * @return
	 * 		A XML comment
	 */
	public XmlComment createComment(Object content) {
		XmlComment comment = new XmlComment();
		comment.content = content;
		return comment;
	}

	/**
	 * Get Validator chain initialized in constructor.
	 * 
	 * @return
	 * 		Validator chain
	 */
	public ValidatorChain getValidator() {
		return validator;
	}

}